/*-------------------------------------------------------------------------
 *
 * execCurrent.c
 *	  支持 WHERE CURRENT OF 游标的执行器
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *	src/backend/executor/execCurrent.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/genam.h"
#include "access/relscan.h"
#include "access/sysattr.h"
#include "catalog/pg_type.h"
#include "executor/executor.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/portal.h"
#include "utils/rel.h"


static char *fc_fetch_cursor_param_value(ExprContext *fc_econtext, int fc_paramId);
static ScanState *fc_search_plan_tree(PlanState *fc_node, Oid fc_table_oid,
								   bool *fc_pending_rescan);


/*
 * execCurrentOf
 *
 * 给定一个 CURRENT OF 表达式和一个表的 OID，确定 CURRENT OF 所命名的游标当前正在扫描表中的哪一行，并将该行的 TID 返回到 *current_tid。
 *
 * 如果识别出了一行，则返回 true。如果游标对该表有效但当前没有扫描该表的一行（在继承情况下这是合法的情况），则返回 false。如果游标不是指定表的有效可更新扫描，则引发错误。
 */
bool execCurrentOf(CurrentOfExpr *fc_cexpr,
			  ExprContext *fc_econtext,
			  Oid fc_table_oid,
			  ItemPointer fc_current_tid)
{
	char	   *fc_cursor_name;
	char	   *fc_table_name;
	Portal		fc_portal;
	QueryDesc  *fc_queryDesc;

	/* 获取游标名称 --- 可能需要查找参数引用 */
	if (fc_cexpr->cursor_name)
		fc_cursor_name = fc_cexpr->cursor_name;
	else
		fc_cursor_name = fc_fetch_cursor_param_value(fc_econtext, fc_cexpr->cursor_param);

	/* 获取表名以可能用于错误消息 */
	fc_table_name = get_rel_name(fc_table_oid);
	if (fc_table_name == NULL)
		elog(ERROR, "cache lookup failed for relation %u", fc_table_oid);

	/* 查找游标的门户 */
	fc_portal = GetPortalByName(fc_cursor_name);
	if (!PortalIsValid(fc_portal))
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_CURSOR),
				 errmsg("cursor \"%s\" does not exist", fc_cursor_name)));

	/*
	 * 我们必须注意非 SELECT 查询以及被保持的游标，这两者可能都有 null queryDesc。
	 */
	if (fc_portal->strategy != PORTAL_ONE_SELECT)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_CURSOR_STATE),
				 errmsg("cursor \"%s\" is not a SELECT query",
						fc_cursor_name)));
	fc_queryDesc = fc_portal->queryDesc;
	if (fc_queryDesc == NULL || fc_queryDesc->estate == NULL)
#ifdef FDD //cppcheck
	{//cppcheck
#endif
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_CURSOR_STATE),
				 errmsg("cursor \"%s\" is held from a previous transaction",
						fc_cursor_name)));
#ifdef FDD //cppcheck
		return false;
	}
#endif
	/*
	 * 我们有两种不同的策略，取决于游标是否使用 FOR UPDATE/SHARE。支持这两者的原因是，
	 * FOR UPDATE 代码能够在许多情况下识别目标表，而其他代码不能，而非 FOR UPDATE 的情况允许使用带有不敏感游标的 WHERE CURRENT OF。
	 */
	if (fc_queryDesc->estate->es_rowmarks)
	{
		ExecRowMark *fc_erm;
		Index		fc_i;

		/*
		 * 在这里，查询必须对目标表有一个唯一的 FOR UPDATE/SHARE 引用，我们从中挖掘 ctid 信息。
		 */
		fc_erm = NULL;
		for (fc_i = 0; fc_i < fc_queryDesc->estate->es_range_table_size; fc_i++)
		{
			ExecRowMark *fc_thiserm = fc_queryDesc->estate->es_rowmarks[fc_i];

			if (fc_thiserm == NULL ||
				!RowMarkRequiresRowShareLock(fc_thiserm->markType))
				continue;		/* 忽略非 FOR UPDATE/SHARE 项 */

			if (fc_thiserm->relid == fc_table_oid)
			{
				if (fc_erm)
					ereport(ERROR,
							(errcode(ERRCODE_INVALID_CURSOR_STATE),
							 errmsg("cursor \"%s\" has multiple FOR UPDATE/SHARE references to table \"%s\"",
									fc_cursor_name, fc_table_name)));
				fc_erm = fc_thiserm;
			}
		}

		if (fc_erm == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_CURSOR_STATE),
					 errmsg("cursor \"%s\" does not have a FOR UPDATE/SHARE reference to table \"%s\"",
							fc_cursor_name, fc_table_name)));

		/*
		 * 游标必须有一个当前结果行：根据 SQL 规范，如果没有则是一个错误。
		 */
		if (fc_portal->atStart || fc_portal->atEnd)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_CURSOR_STATE),
					 errmsg("cursor \"%s\" is not positioned on a row",
							fc_cursor_name)));

		/* 返回当前扫描的 TID（如果有的话） */
		if (ItemPointerIsValid(&(fc_erm->curCtid)))
		{
			*fc_current_tid = fc_erm->curCtid;
			return true;
		}

		/*
		 * 这个表没有生成游标的当前行；可能是同一父类的其他继承子类生成了。通知调用者在此表上不执行任何操作。
		 */
		return false;
	}
	else
	{
		/*
		 * 没有 FOR UPDATE，我们将查找游标计划中的扫描节点。如果不存在或者被聚合埋在下面则失败。
		 */
		ScanState  *fc_scanstate;
		bool		fc_pending_rescan = false;

		fc_scanstate = fc_search_plan_tree(fc_queryDesc->planstate, fc_table_oid,
									 &fc_pending_rescan);
		if (!fc_scanstate)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_CURSOR_STATE),
					 errmsg("cursor \"%s\" is not a simply updatable scan of table \"%s\"",
							fc_cursor_name, fc_table_name)));

		/*
		 * 游标必须有当前结果行：根据SQL规范，如果没有则是一个错误。我们在顶层测试这个，而不是在扫描节点级别，因为在继承情况下，任何一个表扫描都可能没有位于某行上。如果传入的表OID是某个非活动扫描的，我们希望返回false，而不是引发错误。
		 */
		if (fc_portal->atStart || fc_portal->atEnd)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_CURSOR_STATE),
					 errmsg("cursor \"%s\" is not positioned on a row",
							fc_cursor_name)));

		/*
		 * 如果我们发现了一个非活动扫描，现在可以安全地返回false。它是非活动的，要么没有定位在某行上，要么有待处理的重新扫描。
		 */
		if (TupIsNull(fc_scanstate->ss_ScanTupleSlot) || fc_pending_rescan)
			return false;

		/*
		 * 提取扫描当前行的TID。这个机制原则上依赖于扫描类型，但是对于大多数扫描类型，我们可以直接从物理扫描元组中提取TID。
		 */
		if (IsA(fc_scanstate, IndexOnlyScanState))
		{
			/*
			 * 对于IndexOnlyScan，存储在ss_ScanTupleSlot中的元组可能是一个没有ctid列的虚拟元组，因此我们必须从xs_ctup.t_self中获取TID。
			 */
			IndexScanDesc fc_scan = ((IndexOnlyScanState *) fc_scanstate)->ioss_ScanDesc;

			*fc_current_tid = fc_scan->xs_heaptid;
		}
		else
		{
			/*
			 * 默认情况：尝试从扫描节点的当前元组中获取TID。作为额外的交叉检查，验证当前元组中的tableoid。如果扫描没有提供物理元组，我们必须失败。
			 */
			Datum		fc_ldatum;
			bool		fc_lisnull;
			ItemPointer fc_tuple_tid;

#ifdef USE_ASSERT_CHECKING
			fc_ldatum = slot_getsysattr(fc_scanstate->ss_ScanTupleSlot,
									 TableOidAttributeNumber,
									 &fc_lisnull);
			if (fc_lisnull)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_CURSOR_STATE),
						 errmsg("cursor \"%s\" is not a simply updatable scan of table \"%s\"",
								fc_cursor_name, fc_table_name)));
			Assert(DatumGetObjectId(fc_ldatum) == fc_table_oid);
#endif

			fc_ldatum = slot_getsysattr(fc_scanstate->ss_ScanTupleSlot,
									 SelfItemPointerAttributeNumber,
									 &fc_lisnull);
			if (fc_lisnull)
				ereport(ERROR,
						(errcode(ERRCODE_INVALID_CURSOR_STATE),
						 errmsg("cursor \"%s\" is not a simply updatable scan of table \"%s\"",
								fc_cursor_name, fc_table_name)));
			fc_tuple_tid = (ItemPointer) DatumGetPointer(fc_ldatum);

			*fc_current_tid = *fc_tuple_tid;
		}

		Assert(ItemPointerIsValid(fc_current_tid));

		return true;
	}
}

/*
 * fetch_cursor_param_value
 *
 * 获取参数的字符串值，验证其类型为REFCURSOR。
 */
static char * fc_fetch_cursor_param_value(ExprContext *fc_econtext, int fc_paramId)
{
	ParamListInfo fc_paramInfo = fc_econtext->ecxt_param_list_info;

	if (fc_paramInfo &&
		fc_paramId > 0 && fc_paramId <= fc_paramInfo->numParams)
	{
		ParamExternData *fc_prm;
		ParamExternData fc_prmdata;

		/* 给钩子一个机会，以防参数是动态的 */
		if (fc_paramInfo->paramFetch != NULL)
			fc_prm = fc_paramInfo->paramFetch(fc_paramInfo, fc_paramId, false, &fc_prmdata);
		else
			fc_prm = &fc_paramInfo->params[fc_paramId - 1];

		if (OidIsValid(fc_prm->ptype) && !fc_prm->isnull)
		{
			/* 安全检查，以防钩子做了意外的事情 */
			if (fc_prm->ptype != REFCURSOROID)
				ereport(ERROR,
						(errcode(ERRCODE_DATATYPE_MISMATCH),
						 errmsg("type of parameter %d (%s) does not match that when preparing the plan (%s)",
								fc_paramId,
								format_type_be(fc_prm->ptype),
								format_type_be(REFCURSOROID))));

			/* 我们知道refcursor使用文本的输入/输出例程 */
			return TextDatumGetCString(fc_prm->value);
		}
	}

	ereport(ERROR,
			(errcode(ERRCODE_UNDEFINED_OBJECT),
			 errmsg("no value found for parameter %d", fc_paramId)));
	return NULL;
}

/*
 * search_plan_tree
 *
 * 在指定表的PlanState树中搜索扫描节点。
 * 如果未找到或多个候选项则返回NULL。
 *
 * 注意：此函数不仅仅用来寻找某个候选扫描，同时要确保该扫描返回了计划树的当前输出行。这就是为什么我们必须拒绝多个匹配的情况。
 *
 * 如果找到候选项，如果该候选项或其上面的任何节点有待处理的重新扫描操作，即chgParam != NULL，则将*pending_rescan设置为true。这表明，尽管其自身的状态可能表明它是有效的，我们也不应考虑该节点处于有效元组上。（调用者必须将*pending_rescan初始化为false，并且如果找到多个候选项则不应相信其状态。）
 */
static ScanState *
fc_search_plan_tree(PlanState *fc_node, Oid fc_table_oid,
				 bool *fc_pending_rescan)
{
	ScanState  *fc_result = NULL;

	if (fc_node == NULL)
		return NULL;
	switch (nodeTag(fc_node))
	{
			/*
			 * 关系扫描节点可以相同处理：检查它们是否正在扫描指定的表。
			 *
			 * ForeignScan和CustomScan可能没有currentRelation，在这种情况下我们忽略它们。（我们不敢深入它们可能拥有的任何子计划节点，因为我们不知道该节点的当前输出元组与子节点当前输出的关系。）
			 */
		case T_SeqScanState:
		case T_SampleScanState:
		case T_IndexScanState:
		case T_IndexOnlyScanState:
		case T_BitmapHeapScanState:
		case T_TidScanState:
		case T_TidRangeScanState:
		case T_ForeignScanState:
		case T_CustomScanState:
			{
				ScanState  *fc_sstate = (ScanState *) fc_node;

				if (fc_sstate->ss_currentRelation &&
					RelationGetRelid(fc_sstate->ss_currentRelation) == fc_table_oid)
					fc_result = fc_sstate;
				break;
			}

			/*
			 * 对于Append，我们可以检查每个输入节点。下到输入中是安全的，因为只有发生了Append当前输出节点的输入才能被定位到某个元组上；其他输入要么在EOF，要么尚未开始。因此，如果某个当前非活动的输入节点扫描了所需的表，我们会找到该节点，但随后我们的调用者会意识到它没有发出相关的元组。
			 *
			 * 我们确实需要注意多个匹配（如果Append是来自UNION ALL而不是继承树则可能发生）。
			 *
			 * 注意：我们不能通过MergeAppend类似地进行下降，因为其输入可能都是活动的，而我们不知道哪个返回了当前的输出元组。（也许如果我们让此代码更多地了解MergeAppend的内部状态，那么问题可以解决，但这似乎不值得麻烦。用户不应期望ORDER BY查询的计划被认为是简单可更新的，因为如果排序是由Sort节点实现的，它们就不会。）
			 */
		case T_AppendState:
			{
				AppendState *fc_astate = (AppendState *) fc_node;
				int			fc_i;

				for (fc_i = 0; fc_i < fc_astate->as_nplans; fc_i++)
				{
					ScanState  *fc_elem = fc_search_plan_tree(fc_astate->appendplans[fc_i],
														fc_table_oid,
														fc_pending_rescan);

					if (!fc_elem)
						continue;
					if (fc_result)
						return NULL;	/* 多个匹配 */
					fc_result = fc_elem;
				}
				break;
			}

			/*
			 * Result和Limit可以进行下降（这些是安全的，因为它们总是返回其输入的当前行）
			 */
		case T_ResultState:
		case T_LimitState:
			fc_result = fc_search_plan_tree(fc_node->lefttree,
									  fc_table_oid,
									  fc_pending_rescan);
			break;

			/*
			 * SubqueryScan也是如此，但它在不同的位置保留子节点
			 */
		case T_SubqueryScanState:
			fc_result = fc_search_plan_tree(((SubqueryScanState *) fc_node)->subplan,
									  fc_table_oid,
									  fc_pending_rescan);
			break;

		default:
			/* 否则，假设我们不能在其中下降 */
			break;
	}

	/*
	 * 如果我们在这个节点或其下方找到候选项，那么该节点的chgParam表示将影响该候选项的待处理的重新扫描。
	 */
	if (fc_result && fc_node->chgParam != NULL)
		*fc_pending_rescan = true;

	return fc_result;
}
